//
// heap
//
// heap is for collecting commands used in the shell into
// stacks which you can then replay in sequential order.
// This is handy for builing up a set of commands
// to help you create a test case
//
// Author: David Dahl <ddahl@mozilla.com>
//

if(Ci === undefined){
  var Ci = Components.interfaces;
  var Cc = Components.classes;
  var Cu = Components.utils;
}

var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
// XXX: import FileIO via Components.utils.import( nsIFile.spec );

var DEBUG = false;

var echo = function (data,klass){
  // wrapper for mozshell.outputWrite
  try {
    var _out = document.createElement('div');
    _out.className = 'message';
    _out.appendChild(document.createTextNode(data));
    window.mozshell.output.appendChild(_out);
  }
  catch(e) {
    dump(e);
    alert("Error: echo function cannot write to mozshell.outputWrite.");
  }
};

var execute = function (input){
  // wrapper for mozshell.execute
  try {
    if (input) {
      window.mozshell.input.value = input;
      window.mozshell.execute();
    } else {
      window.mozshell.execute();
    }
  }
  catch(e) {
    dump(e);
    alert("Error: execute function cannot use mozshell.execute.");
  }
};

var heap = {

  debugToggle: function(){
    if (DEBUG){
      DEBUG = false;
      echo("***debug is OFF");
    }
    else {
      DEBUG = true;
      echo("***debug is ON");
    }
  },

  cmds: {},

  currentCmd: null,

  cmdlist: function(idx){
    if (idx === undefined){
      echo("Current heap stacks: ");
      for (var stk in this.cmds){
        echo("Stack: " + stk);
        echo("---------------------------");
        for (var i=0; i < this.cmds[stk].length; i++){
          echo('[' + i + ']:' + this.cmds[stk][i] + ";");
        }
        echo("============================");
      }
    }
    else {
      echo('Current heap stack, ' + this.currentCmd  + ': ');
      for (var i=0; i < this.cmds[this.currentCmd].length; i++){
        echo('[' + i + ']:' + this.cmds[this.currentCmd][i] + ";");
      }
    }
  },

  new: function(name, initialCmd) {
    if (!this.cmds.name){
      this.cmds[name] = [];
      this.cmds[name].push(initialCmd);
      this.currentCmd = name;
    }
    else {
      echo("{n} is already registered as a stack, choose another name"
              .supplant({n:name}),'error');
    }
  },

  add: function(cmd,name) {
    try {
      // check for current command
      if (name === undefined){
        this.cmds[this.currentCmd].push(cmd);
      } else {
        this.cmds[name].push(cmd);
      }
    } catch(e) {
      // command does not exists throw error
      echo(e);
    }
  },

  removeIdx: undefined,

  remove: function(idx) {
    // try to remove stack item at idx
    this.removeIdx = idx;
    if (idx !== undefined) {
      try {
        this.trashAdd('removed item from stack',
                      this.cmds[this.currentCmd][idx]);
        this.cmds[this.currentCmd].splice(idx,1);
      }
      catch(e) {
        // idx doesnt exist?
        echo("remove error: " + e,'error');
        this.removeChoices();
      }

    }
    else {
      // not sure if it exists?
      this.removeChoices();
    }

  },

  removeChoices: function() {
    // show the list indexes and stack items to remove
    echo("Your attempt to remove a stack item failed, here is the current state of the stack: ");
    this.cmdlist(this.removeIdx);
  },

  runIdx: null,

  runStk: null,

  next: function(cmdStk){
    // run the stack but require 'next()' to continue
    if (cmdStk) {
      if(!this.runIdx && !this.runStk) {
        // first time through - initialize it
        this.runStk = cmdStk;
        this.runIdx = 1;
        execute(this.cmds[this.runStk][0]);
      }
    }
    else if(this.runIdx && this.runStk) {
      try {
        execute(this.cmds[this.runStk][this.runIdx]);
      }
      catch(e) {
        echo("Error: Cannot run {cmd}"
                .supplant({cmd:this.cmds[this.runStk][this.runIdx]}),
                'error');
        }
      }

    this.cmdIdx++;
    if (this.cmdIdx >= this.cmds[this.runStk].length){
      //the run is over!
      this.cmdIdx = null;
      this.cmdStk = null;
    }
  },

  run: function(cmdName) {
    // run the stack
    if (!cmdName && this.currentCmd) {
      // assemble commands
      var cmd = this.currentCmd;
    }
    else {
      var cmd = cmdName;
    }
    try {
      var c = this.cmds[cmd].join("; \n") + ';';
      if (DEBUG){
        echo("DEBUG: Commands:  {cmds}".supplant({cmds:c}));
      }
    }
    catch(e) {
      if (e.name === 'TypeError'){
        echo("Commands not found. Did you define them?",'error');
      }
      else {
        throw(e);
      }
    }
    execute(c);
  },

  trashContents: [{msg:'Trash keeps track of items removed from stacks',
                   val:null}],

  trash: function(){
    // display trash
    for (var i=0; i < this.trashContents.length; i++){
      echo('**** Trash Contents ****');
      echo(this.trashContents[i].toString());
      echo('************************');
    }
  },

  trashAdd: function(msg,val){
    this.trashContents.push({msg:msg,val:val});
  },

  current: function(cmd) {
    if (this.cmds[cmd]){
    this.currentCmd = cmd;
    }
    else {
      echo("No command called " + cmd, "error");
    }
  },

  heapBackupFileName: '.HeapStacksDataBackup.json',

  heapJSONFileName: '.HeapStacksData.json',

  dataSavePath: null,

  dataFileObj: null,

  openDataFile: function() {
    var profD = DirIO.get('ProfD');
    if(profD.isDirectory() && profD.isWritable()) {
      var p = profD.path;
      var sub = {path:p,sep:DirIO.sep,file:heap.heapJSONFileName};
      this.dataSavePath = "{path}{sep}{file}".supplant(sub);
      this.dataFileObj = FileIO.open(this.dataSavePath);
      return true;
    }
    else {
      echo("Cannot write to path", 'error');
      return false;
    }

  },

  save: function() {
    // save the cmds to a textfile,
    // use nsiFile via io.js and JSON --> added to the file below
    this.openDataFile();
    if (this.dataFileObj){
      // save the cmdStacks as json data to the file:
      try {
        var json = JSON.encode(this.cmds);
      }
      catch(e) {
        echo(e,'error');
        return false;
      }
      try {
        var rv = FileIO.write(this.dataFileObj, json);
        return true;
      }
      catch (e){
        echo(e,'error');
        return false;
      }
    }
    else {
      echo('Cannot get data file for writing');
      return false;
    }
  },

  load: function(){
    // check for existing stacks on disk and load them up.
    this.openDataFile();
    try {
      var str = FileIO.read(this.dataFileObj);
      var json = JSON.decode(str);
      this.cmds = json;
      echo("The data file has been loaded successfully.");
      // XXX: call setCurrentCmd()
      return true;
    }
    catch(e) {
      echo(e,'error');
      echo("Could not open data file");
      return false;
    }
  },

  setCurrentCmd: function() {
    // after a load, prompt user to choose the "currentCmd"

  },

  backup: function(fileName){
    // backup the current data - prompt for a name and save as JSON to disk
  },

  saveOnUnload: function(){
    // prompt the user to save data on close of shell
  },

  // heap current window reference
  currentWindow: null,

  // current index of JS Shell enumWins
  currentEnumWin: null,

  setWindow: function(val) {
    // move this functionality back into mozshell
    shellCommands.enumerateWindows();
    // set the current window
    if (typeof val === 'number'){
      go(scope(Shell.enumWins[val]));
      this.currentWindow = Shell.enumWins[val];
    }
    else if (typeof val === 'string'){
      // search the windows for this title:
      var foundWindows = [];
      for(var i=0; i < Shell.enumWins.length; i++){
        if (Shell.enumWins[i].document.title === val){
          var win = Shell.enumWins[i];
          foundWindows.push(String(win));
        }
      }
      if (foundWindows.length === 1){
        echo("Setting current window to {win}".
                supplant({win:foundWindows[0]}));
        go(scope(Shell.enumWins[0]));
      }
      else if (foundWindows.length > 1){
        echo("Found the following windows:");
        for (var win in foundWindows){
          echo(win);
        }
      }
      else {
        // no windows found
        echo("No windows found with title: {title}".
                supplant({title:val}));
      }
    }
  },

  example: function() {
    // create an example stack and run it:
    echo('-- Example Stack ----------');
    this.runCommand('heap.new(\'myExampleStack\',\'var foo = 72\')');
    this.runCommand('heap.add(\'var bar  = 19\')');
    this.runCommand('heap.add(\'var result = bar + foo\')');
    this.runCommand('heap.run()');
  },

  runCommand: function(cmd){
    window.mozshell.input.value = cmd;
    execute(cmd);
  },

  help: function(){
    // display help info
    echo('---------------------------');
    echo('-- heap: help -------------');
    echo('---------------------------');
    echo('1. Create a new \"stack\"');
    echo('heap.new(\'myStackName\',\'command\')');
    echo('**Note: myStackName is now the default stack');
    echo('**Note: all heap commands operate on default stack, unless stackName is provided, which is usually the second argument');
    echo('2. Add commands to the stack');
    echo('heap.add(\'command\',\'stackName (optional)\')');
    echo('3. Run the stack:');
    echo('heap.run(\'stackName (optional)\')');
    echo('4. Remove a command:');
    echo('heap.remove(stackIndex,\'stackName (optional)\')');
    echo('-- Example Stack -----------');
    echo('heap.example()');
    echo('----------------------------');
    echo('heap.js author: ddahl@mozilla.com');
  }

};

if(String.prototype.supplant === undefined){
  // taken from http://javascript.crockford.com/remedial.html
  String.prototype.supplant = function (o) {
    return this.replace(/{([^{}]*)}/g,
      function (a, b) {
        var r = o[b];
        return typeof r === 'string' || typeof r === 'number' ? r : a;
      }
    );
  };
}

if(String.prototype.trim === undefined){
  String.prototype.trim = function () {
    return this.replace(/^\s+|\s+$/g, "");
  };
}

//
// it would make a lot of sense to build a 'plugin' module spec for the shell
// I would make it similar to the middleware module in django.
// you create an object that conforms to an interface that
// takes in the raw command allowing the author to parse and
// massage the input string
// then return the data to an output filter instead of it's stdout in order to format it
//


// io functionality

/////////////////////////////////////////////////
/////////////////////////////////////////////////
//
// Basic JavaScript File and Directory IO module
// By: MonkeeSage, v0.1
//
/////////////////////////////////////////////////
/////////////////////////////////////////////////


if (typeof(JSIO) != 'boolean') {

	var JSIO = true;

	/////////////////////////////////////////////////
	// Basic file IO object based on Mozilla source
	// code post at forums.mozillazine.org
	/////////////////////////////////////////////////

	// Example use:
	// var fileIn = FileIO.open('/test.txt');
	// if (fileIn.exists()) {
	// 	var fileOut = FileIO.open('/copy of test.txt');
	// 	var str = FileIO.read(fileIn);
	// 	var rv = FileIO.write(fileOut, str);
	// 	alert('File write: ' + rv);
	// 	rv = FileIO.write(fileOut, str, 'a');
	// 	alert('File append: ' + rv);
	// 	rv = FileIO.unlink(fileOut);
	// 	alert('File unlink: ' + rv);
	// }

	var FileIO = {

		localfileCID  : '@mozilla.org/file/local;1',
		localfileIID  : Components.interfaces.nsILocalFile,

		finstreamCID  : '@mozilla.org/network/file-input-stream;1',
		finstreamIID  : Components.interfaces.nsIFileInputStream,

		foutstreamCID : '@mozilla.org/network/file-output-stream;1',
		foutstreamIID : Components.interfaces.nsIFileOutputStream,

		sinstreamCID  : '@mozilla.org/scriptableinputstream;1',
		sinstreamIID  : Components.interfaces.nsIScriptableInputStream,

		suniconvCID   : '@mozilla.org/intl/scriptableunicodeconverter',
		suniconvIID   : Components.interfaces.nsIScriptableUnicodeConverter,

		open   : function(path) {
			try {
				var file = Components.classes[this.localfileCID]
								.createInstance(this.localfileIID);
				file.initWithPath(path);
				return file;
			}
			catch(e) {
				return false;
			}
		},

		read   : function(file, charset) {
			try {
				var data     = new String();
				var fiStream = Components.classes[this.finstreamCID]
									.createInstance(this.finstreamIID);
				var siStream = Components.classes[this.sinstreamCID]
									.createInstance(this.sinstreamIID);
				fiStream.init(file, 1, 0, false);
				siStream.init(fiStream);
				data += siStream.read(-1);
				siStream.close();
				fiStream.close();
				if (charset) {
					data = this.toUnicode(charset, data);
				}
				return data;
			}
			catch(e) {
				return false;
			}
		},

		write  : function(file, data, mode, charset) {
			try {
				var foStream = Components.classes[this.foutstreamCID]
									.createInstance(this.foutstreamIID);
				if (charset) {
					data = this.fromUnicode(charset, data);
				}
				var flags = 0x02 | 0x08 | 0x20; // wronly | create | truncate
				if (mode == 'a') {
					flags = 0x02 | 0x10; // wronly | append
				}
				foStream.init(file, flags, 0664, 0);
				foStream.write(data, data.length);
				// foStream.flush();
				foStream.close();
				return true;
			}
			catch(e) {
				return false;
			}
		},

		create : function(file) {
			try {
				file.create(0x00, 0664);
				return true;
			}
			catch(e) {
				return false;
			}
		},

		unlink : function(file) {
			try {
				file.remove(false);
				return true;
			}
			catch(e) {
				return false;
			}
		},

		path   : function(file) {
			try {
				return 'file:///' + file.path.replace(/\\/g, '\/')
							.replace(/^\s*\/?/, '').replace(/\ /g, '%20');
			}
			catch(e) {
				return false;
			}
		},

		toUnicode   : function(charset, data) {
			try{
				var uniConv = Components.classes[this.suniconvCID]
									.createInstance(this.suniconvIID);
				uniConv.charset = charset;
				data = uniConv.ConvertToUnicode(data);
			}
			catch(e) {
				// foobar!
			}
			return data;
		},

		fromUnicode : function(charset, data) {
			try {
				var uniConv = Components.classes[this.suniconvCID]
									.createInstance(this.suniconvIID);
				uniConv.charset = charset;
				data = uniConv.ConvertFromUnicode(data);
				// data += uniConv.Finish();
			}
			catch(e) {
				// foobar!
			}
			return data;
		}

	};


	/////////////////////////////////////////////////
	// Basic Directory IO object based on JSLib
	// source code found at jslib.mozdev.org
	/////////////////////////////////////////////////

	// Example use:
	// var dir = DirIO.open('/test');
	// if (dir.exists()) {
	// 	alert(DirIO.path(dir));
	// 	var arr = DirIO.read(dir, true), i;
	// 	if (arr) {
	// 		for (i = 0; i < arr.length; ++i) {
	// 			alert(arr[i].path);
	// 		}
	// 	}
	// }
	// else {
	// 	var rv = DirIO.create(dir);
	// 	alert('Directory create: ' + rv);
	// }

	// ---------------------------------------------
	// ----------------- Nota Bene -----------------
	// ---------------------------------------------
	// Some possible types for get are:
	// 	'ProfD'				= profile
	// 	'DefProfRt'			= user (e.g., /root/.mozilla)
	// 	'UChrm'				= %profile%/chrome
	// 	'DefRt'				= installation
	// 	'PrfDef'				= %installation%/defaults/pref
	// 	'ProfDefNoLoc'		= %installation%/defaults/profile
	// 	'APlugns'			= %installation%/plugins
	// 	'AChrom'				= %installation%/chrome
	// 	'ComsD'				= %installation%/components
	// 	'CurProcD'			= installation (usually)
	// 	'Home'				= OS root (e.g., /root)
	// 	'TmpD'				= OS tmp (e.g., /tmp)

	var DirIO = {

		sep        : '/',

		dirservCID : '@mozilla.org/file/directory_service;1',

		propsIID   : Components.interfaces.nsIProperties,

		fileIID    : Components.interfaces.nsIFile,

		get    : function(type) {
			try {
				var dir = Components.classes[this.dirservCID]
								.createInstance(this.propsIID)
								.get(type, this.fileIID);
				return dir;
			}
			catch(e) {
              echo(e);
			  return false;
			}
		},

		open   : function(path) {
			return FileIO.open(path);
		},

		create : function(dir) {
			try {
				dir.create(0x01, 0664);
				return true;
			}
			catch(e) {
				return false;
			}
		},

		read   : function(dir, recursive) {
			var list = new Array();
			try {
				if (dir.isDirectory()) {
					if (recursive == null) {
						recursive = false;
					}
					var files = dir.directoryEntries;
					list = this._read(files, recursive);
				}
			}
			catch(e) {
				// foobar!
			}
			return list;
		},

		_read  : function(dirEntry, recursive) {
			var list = new Array();
			try {
				while (dirEntry.hasMoreElements()) {
					list.push(dirEntry.getNext()
									.QueryInterface(FileIO.localfileIID));
				}
				if (recursive) {
					var list2 = new Array();
					for (var i = 0; i < list.length; ++i) {
						if (list[i].isDirectory()) {
							files = list[i].directoryEntries;
							list2 = this._read(files, recursive);
						}
					}
					for (i = 0; i < list2.length; ++i) {
						list.push(list2[i]);
					}
				}
			}
			catch(e) {
			   // foobar!
			}
			return list;
		},

		unlink : function(dir, recursive) {
			try {
				if (recursive == null) {
					recursive = false;
				}
				dir.remove(recursive);
				return true;
			}
			catch(e) {
				return false;
			}
		},

		path   : function (dir) {
			return FileIO.path(dir);
		},

		split  : function(str, join) {
			var arr = str.split(/\/|\\/), i;
			str = new String();
			for (i = 0; i < arr.length; ++i) {
				str += arr[i] + ((i != arr.length - 1) ?
										join : '');
			}
			return str;
		},

		join   : function(str, split) {
			var arr = str.split(split), i;
			str = new String();
			for (i = 0; i < arr.length; ++i) {
				str += arr[i] + ((i != arr.length - 1) ?
										this.sep : '');
			}
			return str;
		}

	};

	if (navigator.platform.toLowerCase().indexOf('win') > -1) {
		DirIO.sep = '\\';
	}

}
